// nth-graph jquery plugin
// author: Nate Morse copyright 2009-2010
// BSD license
// version: 0.06
// project site: http://code.google.com/p/nth-graph/
// source repository:  
// http://nth-graph.googlecode.com/svn/trunk/nth-graph/interpreters/client/jQuery
// usage: 
//   $("#myElement").nthGraph({options}); // runs Hello World
// options: 
//   input:{key:anyType, ...} keys are matched to node names and 
//       data is stored under the same name. Can be over written with metadata.
//   loggin:{status:off|on, dom_element_id:stringType}
//   cg:object Control Graph in JSON or JSOL
//   cgref:stringType  Url of a Control Graph in JSON 
//     (either a cg or a cgref must be included)
//   warn:function calls a function when a warning is encoutered default:console
//   debug:function called for debuging

(function($) {

	$.fn.nthGraph = function(settings) {
		var config = {"input":{}, 
				"warn":function(msg){alert(msg);},
				"debug":function(msg){}
		};
		
		var nth = {
			graph:{"getProperty":function() {
                                var i, obj = this;
                               for(i = 0; i < arguments.length; i += 1) {
                                        obj = obj[arguments[i]];
                                        if(typeof obj == "undefined") {
                                                return null;
                                        }
                                }
                                return obj;
                        } },
			control_graph: {
				t:null,
				run: function(omee) {
					var $this = $(omee);
					if (config.cg) {$.extend(true, nth.graph, config.cg);}
					var input = $this.metadata ? $.extend(true, {}, config.input, $this.metadata({type:"attr", name:"nth-graph-input"})) : config.input;
					//nth.debug("start run...");
					nth.attachEnv(omee);
					nth.control_graph.traverse();
					nth.detachEnv();
				},
				traverse: function(node_index) {
					node_index = node_index || 0;
					var traverse_limit = (config.traverse_limit)? config.traverse_limit:5000;
					while(traverse_limit > 0 && node_index >= 0) {
					    nth.debug("traverse "+node_index);
						traverse_limit--;
						nth.mergeNodeData(node_index, nth.control_graph.get(node_index));
						nth.control_graph.process(node_index);
						nth.control_graph.set(node_index);
						node_index = nth.control_graph.transition(node_index);
					}
				},
				get: function(node_index) {
					var data = [];
					var outEdges = nth.getOutEdges(node_index, "get");
					$.each(outEdges, function(edge_index, edge_obj) {
						var edge_name = edge_obj.name;
						var edge_alias = edge_obj.alias;
						var to_node_index = edge_obj.to;
						nth.getExternal(to_node_index);
						var new_data;
						if (!edge_name && !edge_alias) {
							new_data = nth.getNodeData(to_node_index);
							data = $.extend(true, data, new_data);
						}
						else { 
							if (!edge_name) {
								edge_name = nth.getNodeAttr(to_node_index, 'name');
							}
							if (!edge_alias) { 
								edge_alias = edge_name;
							}
							new_data = nth.getNodeDataAttr(to_node_index, edge_alias);
							if (edge_obj.enforce && edge_obj.enforce.type == "number") {
								new_data = +new_data;
							}
							//nth.debug("get_data "+to_node_index+" "+edge_alias+":"+new_data);
							data[edge_name] = new_data;
						}
					});
					return data;
				},
				process: function(node_index) {
					// process a QueryGraph
					if(nth.getNodeAttr(node_index, "type") == "queryGraph"){
						//var name = nth.getNodeAttr(node_index, "name");
						//var qg = QueryGraph(name+".qg.json");
						//nth.graph.nodes[node_index].data[name] = qg.query(nth.graph.nodes[node_index].data);
					}
					var node_process = nth.getNodeProcess(node_index);
					if (node_process && typeof node_process == 'string') {
						//  TODO sanitize node_process string to adhere to "function(){}" only!
						// nth.warn("node_process does not adhere to function(){...} --instead: " + node_process);
						//nth.debug(" *node process string = " + node_process)
						eval("node_process = "+node_process + ";");
					}
					if (node_process && typeof node_process == 'function') {
						node_process.call(nth.graph.nodes[node_index].data);
					}
				},
				set: function(node_index) {
					var outEdges = nth.getOutEdges(node_index, "set");
					$.each(outEdges, function(edge_index, edge_obj) {
						var from_node_index = edge_obj.from;
						var edge_name = edge_obj.name;
						var edge_alias = edge_obj.alias;
						var to_node_index = edge_obj.to;
						if (!edge_name) {
							edge_name = nth.getNodeAttr(to_node_index, 'name');
						}
						if (!edge_alias) { 
							edge_alias = edge_name;
						}
						var value = nth.getNodeDataAttr(from_node_index, edge_name);
						nth.setNodeDataAttr(to_node_index, edge_alias, value);
						var to_type = nth.getNodeAttr(to_node_index, "type");
						if (to_type == "out" || to_type == "io") {
							var output = nth.getNodeAttr(to_node_index, "output");
							if (output && output.event.change == "immediately") {
								var element = nth.getNodeAttr(to_node_index, "ele");
								if (output.type == "text") {
									$(nth.control_graph.omee).find(element).text(value);
								}
								if (output.type == "value") {
									$(nth.control_graph.omee).find(element).val(value);
								}
							}
						}
					});
				},
				transition: function(node_index) {
					var to_node_index = -1;
					var outEdges = nth.getOutEdges(node_index, "trans");
					$.each(outEdges, function(edge_index, edge_obj) {
					   //nth.debug("testing trans "+edge_index);
            		   var guard_result = nth.control_graph.guardEval(edge_obj);
					   //nth.debug("guard_result "+guard_result);
            			if (guard_result === true) {
            				to_node_index = edge_obj.to;
            			}
            			else {
                            // Save the destination of the first edge that reports that it is a 
                            // 'default' transition
            				if (guard_result === 'default' && to_node_index == -1) {
            					to_node_index = edge_obj.to;
            				}
            			}
					});
					return to_node_index;
				},
				
				guardEval: function(edge_obj) {
					var edge_guard = nth.getNodeGuard(edge_obj);
					nth.debug("guardEval " + edge_guard);
					if (edge_guard && typeof edge_guard == 'string') {
					   if (edge_guard == 'default') {
			              return "default";
                       }
						//  TODO sanitize edge_guard string to adhere to "function(){}" only!
						// nth.warn("edge_guard does not adhere to function(){...} --instead: " + edge_guard);
						nth.debug(" *edge guard string = " + edge_guard);
						eval("edge_guard = "+edge_guard + ";");
					}
					if (edge_guard && typeof edge_guard == 'function') {
						return edge_guard.call(nth.graph.nodes[edge_obj.from].data);
					}
					else {
			             return "default";
                    }

				},
				
				renderOutputs: function($this) {
					$.each(nth.graph.nodes, function(node_index, node_obj) {
						if (nth.key_exists('type', node_obj) && node_obj.type == 'out' && node_obj.type == 'io') {
							var node_ele = node_obj.ele;
							if (node_ele.indexOf('#')>=0) {
								node_ele = node_ele.substr(node_ele.indexOf('#')+1);
								$(nth.control_graph.omee).find(node_obj.ele).text(nth.graph.nodes[node_index].data[node_ele]);
							}
						}
					});
				}
			},
			warn: function(message) {
				config.warn.call(null, message);
				//alert("warn:"+message);
			},
			debug: function(message) {
				//config.debug.call(null, message);
				//alert("debug:"+message);
			},
		
			loadControlGraph: function (url) {
				// method getJSON
				$.ajax({
						"async": false,
						"url":url, "dataType":"json",
						"success": function(g){
								if (!(g)) {
									nth.warn("failed to decode graph resource or failed to open graph resource "+url);
									return false;
								}
								config.cg = g;
							},
							"error": function (XMLHttpRequest, textStatus, errorThrown) {
							  alert('error:: '+textStatus + errorThrown);
							}
				});
			},
		
			getOutEdges: function(node_index, type) {
				var outEdges = [];
				$.each(nth.graph.edges, function(edge_index, edge_obj) {
					if (edge_obj.from == node_index && edge_obj.type == type) {
						outEdges.push(nth.graph.edges[edge_index]);
					} 
				});
				return outEdges;
			},
			
			getNodeProcess: function(node_index) {
				if (!nth.key_exists(node_index, nth.graph.nodes)) {
					return 'Missing Node '+node_index;}
				if (!nth.key_exists('process', nth.graph.nodes[node_index])) {
					return false;}
				var code = nth.graph.nodes[node_index].process;
				return code;
			},
			
			getNodeGuard: function(edge_obj) {
				if (!nth.key_exists('guard', edge_obj)) {
					return false;}
				return  edge_obj.guard;
			},
			
			getNodeAttr: function(node_index, attr) {
				if (!nth.key_exists(node_index, nth.graph.nodes)) {
					return 'none 1';}
				if (!nth.key_exists(attr, nth.graph.nodes[node_index])) {
					return 'none 2';}
				var dataSet = nth.graph.nodes[node_index][attr];
				return dataSet;
			},
			
			getNodeData: function(node_index) {
				if (!nth.key_exists(node_index, nth.graph.nodes)) {
					return 'none 1';}
				if (!nth.key_exists('data', nth.graph.nodes[node_index])) {
					return 'none 2';}
				var dataSet = nth.graph.nodes[node_index].data;
				return dataSet;
			},
			
			getNodeDataAttr: function(node_index, attr) {
				var dataSet = nth.getNodeData(node_index);
				if (!dataSet || !nth.key_exists(attr, dataSet)) {
					//nth.debug("getNodeDataAttr("+node_index+", "+attr+") fails");
					return 'none 3';
				}
				else if (nth.key_exists("input", nth.graph.nodes[node_index]) &&
								 nth.key_exists("enforce", nth.graph.nodes[node_index].input) &&
								 nth.key_exists("type", nth.graph.nodes[node_index].input.enforce)) {
					// example node attribute: "enforce":{"type":"number"}
					if (nth.graph.nodes[node_index].input.enforce.type == "number") {
						return (+dataSet[attr]);
					}
				}
				return dataSet[attr];
			},
			
			setNodeDataAttr: function(node_index, attr, value) {
				if (!nth.key_exists("data", nth.graph.nodes[node_index])) {
					nth.graph.nodes[node_index].data = {};
				}
				nth.graph.nodes[node_index].data[attr] = value;
			},
			
			attachEnv: function(omee) {
                var duration;
                var event_traversal_start = nth.graph.getProperty("graph", "traversal", "start");
				// save the outer most encapsulating element (omee)
				nth.control_graph.omee = omee;
				// attach input nodes to the DOM by binding events run the nth-graph 
				$.each(nth.graph.nodes, function(node_index, node_obj) {
				    // TODO if a node needs to be loaded (ie a graph encapsulated in a node), load it now...
				    
					if (nth.key_exists("type", node_obj) && (node_obj.type == "in" || node_obj.type == "io")) {
						if (nth.key_exists("input", node_obj) && nth.key_exists("event", node_obj.input) && nth.key_exists("ele", node_obj)) {
							$.each(node_obj.input.event, function(input_event, action) {
							    var start_node_index = (event_traversal_start === null)? node_index: event_traversal_start;
								//nth.debug("binding... $("+nth.control_graph.omee+")find.("+node_obj.ele+").bind("+input_event+", "+nth.control_graph[action]+");");								
								$(nth.control_graph.omee).find(node_obj.ele).bind(input_event, function(e) {nth.graph.nodes[node_index].data['event'] = e; nth.control_graph[action].call(this, start_node_index); });
							});
						}
						if (nth.key_exists("timer", node_obj) && nth.key_exists("action", node_obj.timer)) {
                            nth.timerEvent(node_index, node_obj.timer.action, node_obj.timer.duration || 1000);                            
						}
					} 
				});
			},
			detachEnv: function() {
				// unbind here??
			},
			timerEvent: function(node_index, action, duration)
            {
                //nth.control_graph.traverse(node_index);
                nth.control_graph[action].call(this, node_index)
                var timerDelay = function() { nth.timerEvent(node_index, action, duration); };
                setTimeout(timerDelay, duration);
            },

			getExternal: function(node_index) {
				var node_obj = nth.graph.nodes[node_index];
				if (nth.key_exists('type', node_obj) && (node_obj.type == 'in' || node_obj.type == 'io')) {
					if (nth.key_exists('subtype', node_obj) && 
						node_obj.subtype == 'GET'){
						// $.get from url and then on success ?? should we block async=false ??
						$.getJSON(node_obj.url, {"name":"node_obj.name"}, function(json){
							if (!(json)) {
								alert("failed to get input resource "+node_obj.url);
							}
							//nth.debug("opening "+node_obj.url);
							$.extend(true, nth.graph.nodes[node_index].data, json.data);
						});
					}
					else if (nth.key_exists('subtype', node_obj) && node_obj.subtype == 'POST') {
						// $.post to url and then on success
					}
					else {
						// input is from a graph input or config.input, or a jQuery/DOM source.
						//if (nth.key_exists(node_obj.name, input)) {
						//	nth.graph.nodes[node_index].data[node_obj.name] = input[node_obj.name];
						//}
						//else 
						if (config.input && nth.key_exists(node_obj.name, config.input)) {
							nth.graph.nodes[node_index].data[node_obj.name] = config.input[node_obj.name];
						}
						else 
						if (nth.key_exists('ele', node_obj)) {
						
							if (!nth.graph.nodes[node_index].data) {nth.graph.nodes[node_index].data = {};}
							var node_ele = node_obj.ele;
							if (node_ele.indexOf('#')>=0) {
								node_ele = node_ele.substr(node_ele.indexOf('#')+1);
							}
							if (nth.key_exists('subtype', node_obj) && node_obj.subtype == "checkbox") {
								nth.graph.nodes[node_index].data[node_ele] = $(nth.control_graph.omee).find(node_obj.ele).is(':checked');
							} else {
								nth.graph.nodes[node_index].data[node_ele] = $(nth.control_graph.omee).find(node_obj.ele).val();
							}
						}
					}
				}
			}
			,
			
			mergeNodeData: function(node_index, data) {
				//nth.graph.nodes[node_index].data = 
					nth.graph.nodes[node_index].data = $.extend(true, 
						nth.graph.nodes[node_index].data,
						data
					);
			},
					
			key_exists: function(key_str, obj) {
				if(typeof obj != "undefined" &&  typeof obj[key_str] != "undefined") {
					return true;
				}
				return false;
			}
		};
		
		// now start processing this nthGraph instance.
		if (settings) {$.extend(true, config, settings);}
		if (config.cg && typeof config.cg == 'object') {$.extend(true, nth.graph, config.cg);}
		else if (config.cgref && typeof config.cgref == 'string') {
			nth.loadControlGraph(config.cgref);
		}
		else {nth.warn("No control graph (cg) indicated.");}
		
		// so far this is only tested on one (jQuery selected) element at a time, 
		// but it could be used on an array of elements,
		// Input includes metadata (if the metadata.js lib is included),
		// or input is from form elements. 
		// Output will be applied back to output decendant from the given element. 
		this.each(function() {
			nth.control_graph.run(this);
		});
		
		return this;
	};
})(jQuery);
